;;-------------------------------------------------
;; An openGL UI for the evolol game
;;-------------------------------------------------

(ns evolol.ogl
  (:use clojure.pprint
        evolol.core)
  (:import (java.lang System)
           (com.jogamp.opengl.util.texture Texture
                                           TextureIO
                                           TextureData)
           (java.io BufferedReader
                    InputStreamReader)
           (com.jogamp.opengl.util Animator)
           (com.jogamp.newt Window)
           (javax.media.opengl GLRunnable)
           (com.jogamp.newt.event WindowAdapter
                                  WindowEvent)
           (com.jogamp.opengl.util.awt TextRenderer)
           (java.awt Font)    
           (javax.media.opengl GL
                               GL2
                               GL2ES1
                               GLAutoDrawable
                               GLProfile
                               GLCapabilities
                               GLCapabilitiesImmutable
                               GLEventListener)
           (javax.media.opengl GLRunnable)
           (javax.media.opengl.glu GLU)
           (com.jogamp.newt.opengl GLWindow)
           (com.jogamp.newt.event KeyListener
                                  KeyEvent
                                  MouseListener)
           (com.jogamp.opengl.util FPSAnimator)
           (java.awt GraphicsEnvironment
                     GraphicsDevice
                     DisplayMode
                     Toolkit)
           (java.io FileInputStream)
           (com.jogamp.opengl.util.texture.spi.awt IIOTextureProvider)))


;;;;;;;;;;;;;;;;; RESOURCES ;;;;;;;;;;;;;;;;;;

;; !!! KENNETH !!!  <<< ALERT >>>
;;   change this to the path on your system !!!
(def *cpu-ps-script* "c:\\dev\\Powershell\\junk\\cpujava.ps1")

(def *critter-image* "pm32.png")
(def *plant-image* "apple32.png")

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


(defn quit [] (System/exit 0))

(def knst-ogl-list-sqr 103)
(def finished (atom false))
(def *renderer* (TextRenderer. (Font. "SansSerif" Font/PLAIN 18)
                               true
                               false))
(def *text-flag* (atom false))
(def *sleeper* (atom 10))
(def *powershell-proc* (atom nil))
(def *cpu-pcnt* (ref "0"))
(def *newt-window* (atom nil))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; (def swank-con swank.core.connection/*current-connection*)

;; (defmacro break [] 
;;  `(binding [swank.core.connection/*current-connection* swank-con] 
;;     (swank.core/break)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


;;;;;;;;;;;;;;;;;; MUTABLE bounds state ;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(definterface Ibounds
  (^float getWidth [])
  (^float getHeight [])
  (setWidth_BANG_ [^float w])
  (setHeight_BANG_ [^float h]))

(deftype Tbounds [^{:unsynchronized-mutable true :tag float} w
                  ^{:unsynchronized-mutable true :tag float} h]
  Ibounds

  (getWidth [this] w)
  (getHeight [this] h)
  (setWidth! [this inW]
             (set! w inW)
             this)
  (setHeight! [this inH]
              (set! h inH)
              this))

(def *bounds* (Tbounds. 0.0 0.0))


;;;;;;;;;;;;;;;;;;;; MUTABLE navagation and zoom fields ;;;;;;;;;;;;
(definterface Inavzoom
  (^float getZoom [])
  (^float getNavX [])
  (^float getNavY [])
  (setZoom_BANG_ [^float zoom])
  (setNavX_BANG_ [^float navX])
  (setNavY_BANG_ [^float navY]))

(deftype Tnavzoom [^{:unsynchronized-mutable true :tag float} zoom
                   ^{:unsynchronized-mutable true :tag float} navX
                   ^{:unsynchronized-mutable true :tag float} navY]
  Inavzoom
  
  (getZoom [this] zoom)
  (getNavX [this] navX)
  (getNavY [this] navY)
  (setZoom! [this inZ]
            (set! zoom inZ)
            this)
  (setNavX! [this inX]
            (set! navX inX)
            this)
  (setNavY! [this inY]
            (set! navY inY)
            this))

(def *navzoom* (Tnavzoom. -3.0 -500.0 -300.0))


;;;;;;;;;;;;;;;;;;; MUTABLE image textures ;;;;;;;;;;;;;;;;;;;;;;;;;
(definterface Iimages
  (^com.jogamp.opengl.util.texture.Texture plantTex [])
  (^com.jogamp.opengl.util.texture.Texture critterTex [])
  (plantTex_BANG_ [^com.jogamp.opengl.util.texture.Texture tex])
  (critterTex_BANG_ [^com.jogamp.opengl.util.texture.Texture tex]))

(deftype Timages
  [^{:unsynchronized-mutable true
     :tag com.jogamp.opengl.util.texture.Texture} plantTex
     ^{:unsynchronized-mutable true
       :tag com.jogamp.opengl.util.texture.Texture} critterTex]
  Iimages
  (plantTex [this] plantTex)
  (plantTex! [this tex] (set! plantTex tex))
  (critterTex [this] critterTex)
  (critterTex! [this tex] (set! critterTex tex)))

(def *image-tex* (Timages. nil nil))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;


(defn build-square [ogl]
  ;; create the square ogl list here
  (doto ogl
    (.glNewList knst-ogl-list-sqr (GL2/GL_COMPILE))
    (.glBegin (GL2/GL_QUADS))
    (.glTexCoord2d 1 1)
    (.glVertex3f 0.0 0.0 0.0)
    (.glTexCoord2d 0 1)
    (.glVertex3f 1.0 0.0 0.0)
    (.glTexCoord2d 0 0)
    (.glVertex3f 1.0 1.0 0.0) 
    (.glTexCoord2d 1 0)
    (.glVertex3f 0.0 1.0 0.0)
    (.glEnd)   
    (.glEndList)))

(defn tex-from-io [file-name]
  ;; create a texture from an image file
  (TextureIO/newTexture
   (.newTextureData
    (IIOTextureProvider.)
    (GLProfile/getDefault)
    (FileInputStream.
     (.getFile (ClassLoader/getSystemResource
                file-name)))
    0 0 false nil)))

(defn create-tex [^GL2 gl file-name]
  ;; add ogl tex params to the texture-image
  (let [tex (tex-from-io file-name)]
    (doto tex
      (.setTexParameteri gl GL2/GL_TEXTURE_WRAP_S GL2/GL_CLAMP)
      (.setTexParameteri gl GL2/GL_TEXTURE_WRAP_T GL2/GL_CLAMP))
    tex ))

(defn gl-draw [^GL2 ogl use-image]
  ;; call the evolol control function and then draw the
  ;; plants and animals
  (.glBlendFunc ogl GL/GL_SRC_ALPHA, GL/GL_ONE_MINUS_SRC_ALPHA)
  (.glEnable ogl GL/GL_BLEND)
  
  (dorun (map (fn [animal]
                (.glLoadIdentity ogl)
                (.glTranslatef ogl
                               (+ (get-x animal))
                               (+ (get-y animal))
                               0.0)
                (let [r (- 0.2 (* (/ (get-energy animal) *reproduction-energy*) 0.2))
                      g (+ 0.2 (* (/ (get-energy animal) *reproduction-energy*) 0.8))
                      b (- 0.2 (* (/ (get-energy animal) *reproduction-energy*) 0.2))]
                  (.glColor3f ogl r g b)) ; animals go from low-energy grey to high-energy green
                (if use-image
                  (do
                    (.glEnable ogl GL/GL_TEXTURE_2D)
                    (.bind (.critterTex *image-tex*) ogl )
                    (.enable (.critterTex *image-tex*) ogl)
                    (.glCallList ogl knst-ogl-list-sqr)
                    (.disable (.critterTex *image-tex*) ogl))
                  (do
                    (.glCallList ogl knst-ogl-list-sqr)
                    ;; draw "mouth"
                    ;; this work kinda. mouth sticks out to the right for some reason
                    (.glColor3f ogl 0.0 0.0 0.0)
                    (.glBegin ogl (GL2/GL_LINES))
                    (.glVertex3f ogl (/ 1.0 2.0) (/ 1.0 2.0) 0.0)
                    (let [offset (dir-to-offset (get-dir animal))
                          x (+ (/ 1.0 2.0) (/ (* 1.0 (:x offset)) 2.0))
                          y (+ (/ 1.0 2.0) (/ (* 1.0 (:y offset)) 2.0))]
                      (.glVertex3f ogl (int x) (int y) 0.0))
                    (.glEnd ogl))))              
              (vals @evolol.core/*animals*)))
  (dorun (map
          (fn [pos]
            (.glLoadIdentity ogl)
            (.glTranslatef ogl
                           (+ (pos 0))
                           (+ (pos 1))
                           0.0)
            (.glColor3f ogl 0.6 0.0 0.0)
            (if use-image
              (do
                (.glEnable ogl GL/GL_TEXTURE_2D)
                (.bind (.plantTex *image-tex*) ogl )
                (.enable (.plantTex *image-tex*) ogl)
                (.glCallList ogl knst-ogl-list-sqr)
                (.disable (.plantTex *image-tex*) ogl))
              (.glCallList ogl knst-ogl-list-sqr)))
          (keys @evolol.core/*plants*))))

(defn now []
  ;; in seconds
  (double (/ (. System (nanoTime)) 1000000000.0)))

(def *sec* (atom (now)))
(def *frames* (atom 0.0))
(def *fps* (atom 0.0))

(defn render-text [^GLAutoDrawable drawable]
  ;; guess what this function does!
  (.beginRendering *renderer*
                   (.getWidth drawable)
                   (.getHeight drawable))
  (.draw *renderer*
         (str "sleep (ms) : " @*sleeper* 
              (format "     zoom: %.1f" (.getZoom *navzoom*))
              (format "     navX: %.1f" (.getNavX *navzoom*))
              (format "     navY: %.1f" (.getNavY *navzoom*))
              (format "     fps: %.1f" @*fps*)
              (format "     cpu: %s" @*cpu-pcnt*)) 10 10)
  (.endRendering *renderer*))

(defn count-fps []
  ;; calculate the fps
  (when (>= (- (now) @*sec*) 1.0)
    (reset! *fps* (float (/ @*frames* 1.0)))
    (reset! *sec* (now))
    (reset! *frames* 0))
  (swap! *frames* inc))

(defn trans-projection [^GL2 gl aspect]
  ;; transform the projection matrix for display and reshape
  ;; - implements navigation and zooming.
  (let [glu (GLU.)]
    (.glMatrixMode gl GL2/GL_PROJECTION)
    (.glLoadIdentity gl)
    (.gluPerspective glu (float (+ 179.0 ))
                     (float aspect)
                     (float 0.0)
                     (float 130.0))
    (.glTranslatef gl
                   (.getNavX *navzoom*)
                   (.getNavY *navzoom*)
                   (.getZoom *navzoom*))))

(defn key-mouse-invocation []
  ;; called from a key/mouse event handler: sets up a function that is run 
  ;; once before the display procedure and sends it a valid GL context. This
  ;; allows us to transform projection _only_ in response to a key/mouse event
  (.invoke
   @*newt-window*
   true
   (proxy [GLRunnable] []
     (run [^GLAutoDrawable drawable]
          (let [^GL2 gl (.. drawable getGL getGL2)
                ^GLWindow wnd drawable
                aspect (float (/ (float (.getWidth wnd))
                                 (if (<= (float (.getHeight wnd)) 0)
                                   1
                                   (float (.getHeight wnd)))))]
            (trans-projection gl aspect)
            true)))))

(defn event-listener []
  ;; process the ogl events and mouse and keys
  (proxy [GLEventListener MouseListener KeyListener] []
    
    (init [^GLAutoDrawable drawable]
          (let [^GL2 gl (.. drawable getGL getGL2)
                caps (.getChosenGLCapabilities drawable)
                ^Texture plant-tex (create-tex gl *plant-image*)
                ^Texture critter-tex (create-tex gl *critter-image*)]
            
            (.plantTex! *image-tex* plant-tex)
            (.critterTex! *image-tex* critter-tex)
            (build-square gl)
            (doto gl
              (.setSwapInterval 1) ;Enable VSync
              (.glClearColor 0.0 0.1 0.0 0.0)
              (.glShadeModel GL2/GL_FLAT))))

    (reshape [^GLAutoDrawable drawable x y w h]
             (let [^GL2 gl (.. drawable getGL getGL2)
                   aspect (float (/ w (if (<= h 0) 1 h)))]
               (.glViewport gl 0 0 w h)
               (trans-projection gl aspect)
               (.setWidth! *bounds* w)
               (.setHeight! *bounds* h)
               (.glMatrixMode gl GL2/GL_MODELVIEW)
               (.glLoadIdentity gl)))
    
    (display [^GLAutoDrawable drawable]
             (let [^GL2 gl (.. drawable getGL getGL2)]
               (count-fps)
               (.glMatrixMode gl GL2/GL_MODELVIEW)
               (.glLoadIdentity gl)
               (.glClear gl (GL2/GL_COLOR_BUFFER_BIT))
               (gl-draw gl (>= (.getZoom *navzoom*) -0.7))
               (when @*text-flag* (render-text drawable))
               (.glFlush gl)))
    
    (dispose [drawable]
             (println " *********** disposed *********"))

    (mousePressed [msevt])
    (mouseClicked [msevt])
    (mouseEntered [msevt])
    (mouseExited [msevt])
    (mouseReleased [msevt])
    (mouseMoved [msevt])
    (mouseDragged [msevt])
    
    (mouseWheelMoved [msevt]
                     ;; zoom in / zoom out
                     (.setZoom! *navzoom*
                                (+ (.getZoom *navzoom*)
                                   (/ (.getWheelRotation msevt)
                                      10.0)))
                     (key-mouse-invocation))
    (keyPressed
     ;; q: system exit;  w: destroy main window
     [keyEvent]
     (let [kc (.getKeyCode keyEvent)
           ^Window src (.getSource keyEvent)]
       (condp = kc
           KeyEvent/VK_Q (quit)
           KeyEvent/VK_F4 (do
                            (.sendWindowEvent
                             src
                             WindowEvent/EVENT_WINDOW_DESTROY_NOTIFY)
                            (.destroy src))
           KeyEvent/VK_H (if @*text-flag*
                           (reset! *text-flag* false)
                           (reset! *text-flag* true))
           KeyEvent/VK_MINUS (if-not (= @*sleeper* 0)
                               (swap! *sleeper* dec))
           KeyEvent/VK_EQUALS (swap! *sleeper* inc)
           KeyEvent/VK_F12 (.setFullscreen src (not (.isFullscreen src)))
           KeyEvent/VK_A (do
                           (.setNavX! *navzoom* (- (.getNavX *navzoom*) 4.0))
                           (key-mouse-invocation))           
           KeyEvent/VK_D (do
                           (.setNavX! *navzoom* (+ (.getNavX *navzoom*) 4.0))
                           (key-mouse-invocation))
           KeyEvent/VK_S (do
                           (.setNavY! *navzoom* (- (.getNavY *navzoom*) 4.0))
                           (key-mouse-invocation))
           KeyEvent/VK_W (do
                           (.setNavY! *navzoom* (+ (.getNavY *navzoom*) 4.0))
                           (key-mouse-invocation))
           (println " === key pressed ==="))))

    (keyTyped [keyEvent])
    (keyReleased [keyEvent])))

(defn set-caps [caps]
  ;; bit depth to 24
  (.setDepthBits caps 24)
  caps)

(defn run-cpu-monitor []
  ;; runs a powershell script and gets the script's
  ;; output: the percentage of the cpu usage
  (let [line (atom nil)
        pwr-shell (.exec
                   (Runtime/getRuntime)
                   (into-array String
                               ["powershell"
                                *cpu-ps-script*])) 
        breader (BufferedReader.
                 (InputStreamReader.
                  (.getInputStream pwr-shell)))]
    (reset! *powershell-proc* pwr-shell)
    (while (not @finished)
      (reset! line (.readLine breader))
      (when-not (nil? @line)
        (dosync (ref-set *cpu-pcnt* @line))))))

(defn kicker []
  ;; sets up and runs the ogl display
  (let [caps (new GLCapabilities (GLProfile/getDefault)) 
        newtWind (GLWindow/create (set-caps caps))
        listener (event-listener)
        theAnimator (new Animator)
        world-updater (Thread.
                       (proxy [Runnable] []
                         (run []
                              (while (not @finished)
                                (update-world)
                                (Thread/sleep @*sleeper*) ;sleep between animal updates, instead
                                ))))
        cpu-monitor (Thread.
                     (proxy [Runnable] []
                       (run []
                            (run-cpu-monitor))))]
    (pprint (.toString caps))
    (doto newtWind
      (.addGLEventListener listener)
      (.addMouseListener listener)
      (.addKeyListener listener)
      (.setTitle "evolol")
      (.setSize 680 400)
      (.setPosition 200 100)
      ;;(.setFullscreen true)
      (.requestFocus)
      (.setVisible true)
      (.addWindowListener (proxy [WindowAdapter] []
                            (windowDestroyNotify
                             [evt]
                             (println "  === destroy notify ===")
                             (.start (Thread.
                                      (proxy [Runnable] []
                                        (run []
                                             (.stop theAnimator)
                                             (reset! finished true)
                                             (.destroy @*powershell-proc*)
                                             (.stop cpu-monitor)))))))))
    (.add theAnimator newtWind)
    (.setAnimator newtWind theAnimator)
    (.start theAnimator)
    (.start world-updater)
    (.start cpu-monitor)
    (reset! *newt-window* newtWind)))

(defn go-ogl []
  ;; call this to run the show
  (init-field)
  (defoliate)
  (exterminate)
  (dorun
   (for [i (range 30000)]
     (add-plants)))
  (dorun
   (for [i (range 600)]
     (animals-add i (random-animal))))
  (kicker))

;;(go-ogl)
